Для крупного инвестиционного фонда «Shut Up and Take My Money» нужно провести исследование рынка в области общественного питания города Москвы для выявления интересных особенностей и закономерностей которые в будущем помогут в выборе подходящего инвесторам места.
Задача:
Исходные данные:
Датасет с заведениями общественного питания Москвы, составленный на основе данных сервисов Яндекс Карты и Яндекс Бизнес на лето 2022 года. Информация, размещённая в сервисе Яндекс Бизнес, могла быть добавлена пользователями или найдена в общедоступных источниках. Она носит исключительно справочный характер.
Описание данных
name — название заведения;address — адрес заведения;category — категория заведения, например «кафе», «пиццерия» или «кофейня»;hours — информация о днях и часах работы;lat — широта географической точки, в которой находится заведение;lng — долгота географической точки, в которой находится заведение;rating — рейтинг заведения по оценкам пользователей в Яндекс Картах (высшая оценка — 5.0);price — категория цен в заведении, например «средние», «ниже среднего», «выше среднего» и так далее;avg_bill— строка, которая хранит среднюю стоимость заказа в виде диапазона, например: middle_avg_bill — число с оценкой среднего чека, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Средний счёт»: middle_coffee_cup — число с оценкой одной чашки капучино, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Цена одной чашки капучино»: chain — число, выраженное 0 или 1, которое показывает, является ли заведение сетевым (для маленьких сетей могут встречаться ошибки):district — административный район, в котором находится заведение, например Центральный административный округ;seats — количество посадочных мест.Основателям фонда «Shut Up and Take My Money» принял решение — открыть кофейню в Москве. Заказчик осознает полноту данных о существующей конкуренции на рынке и не боятся её в этой сфере, ведь кофеен в больших городах уже достаточно.
Нужно определить:
Для корректной работы кластеров библиотеки plotly установим пакет обновлений
pip install plotly==5.13.1
Requirement already satisfied: plotly==5.13.1 in /Users/user/opt/anaconda3/lib/python3.9/site-packages (5.13.1) Requirement already satisfied: tenacity>=6.2.0 in /Users/user/opt/anaconda3/lib/python3.9/site-packages (from plotly==5.13.1) (8.0.1) Note: you may need to restart the kernel to use updated packages.
# Импорт библиотек
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
import json
from urllib.request import urlopen
# Загрузим границы административных округов
with urlopen('https://code.s3.yandex.net/data-analyst/admin_level_geomap.geojson') as f:
geo_json = json.load(f)
# Импорт данных
data = pd.read_csv('https://code.s3.yandex.net/datasets/moscow_places.csv')
# Выведем основную информацию о файле
display(data.info())
data.head()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 8406 entries, 0 to 8405 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 8406 non-null object 1 category 8406 non-null object 2 address 8406 non-null object 3 district 8406 non-null object 4 hours 7870 non-null object 5 lat 8406 non-null float64 6 lng 8406 non-null float64 7 rating 8406 non-null float64 8 price 3315 non-null object 9 avg_bill 3816 non-null object 10 middle_avg_bill 3149 non-null float64 11 middle_coffee_cup 535 non-null float64 12 chain 8406 non-null int64 13 seats 4795 non-null float64 dtypes: float64(6), int64(1), object(7) memory usage: 919.5+ KB
None
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | WoWфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | NaN | NaN | NaN | NaN | 0 | NaN |
| 1 | Четыре комнаты | ресторан | Москва, улица Дыбенко, 36, корп. 1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.875801 | 37.484479 | 4.5 | выше среднего | Средний счёт:1500–1600 ₽ | 1550.0 | NaN | 0 | 4.0 |
| 2 | Хазри | кафе | Москва, Клязьминская улица, 15 | Северный административный округ | пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... | 55.889146 | 37.525901 | 4.6 | средние | Средний счёт:от 1000 ₽ | 1000.0 | NaN | 0 | 45.0 |
| 3 | Dormouse Coffee Shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | NaN | Цена чашки капучино:155–185 ₽ | NaN | 170.0 | 0 | NaN |
| 4 | Иль Марко | пиццерия | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.881166 | 37.449357 | 5.0 | средние | Средний счёт:400–600 ₽ | 500.0 | NaN | 1 | 148.0 |
Комментарий:
# Проверим данные на явные дубликаты
data.duplicated().sum()
0
Комментарий:
# Кол-во уникальных названий
data.name.sort_values().nunique()
5614
# Приведем названия к нижнему регистру
data.name = data.name.str.lower()
# Проверим количество уникальных названий
data.name.nunique()
5512
Коменнтарий:
print(*data.category.sort_values().unique().tolist(), sep='\n')
бар,паб булочная быстрое питание кафе кофейня пиццерия ресторан столовая
data.category.nunique()
8
Комментарий:
Всего 8 категорий:
Проверим какие у какой сети сколько ресторанов в каждой категории
# Группируем по названию сети и категории и считаем кол-во заведений для каждой категории
df = data.groupby(['name', 'category'], as_index=False).agg({'address':'count'})
# Посчитем общее число заведений для каждой сети
df['count'] = df.groupby('name')['address'].transform('sum')
# Посчитаем количество категорий для каждой сети
df['count_name_cat'] = df.groupby('name')['category'].transform('count')
df['proc'] = df['address'] / df['count']
df.columns = ['Название сети', 'Категория', 'Кол-во по категории',
'Общее кол-во', 'Кол-во категорий', 'Доля от сети']
# Выведим результаты
df[(df['Кол-во категорий'] > 2) & (df['Общее кол-во'] > 10)]\
.sort_values(['Общее кол-во','Кол-во по категории'], ascending=False)\
.style.format({'Доля от сети': '{:.1%}'}).background_gradient(subset=['Доля от сети'])
| Название сети | Категория | Кол-во по категории | Общее кол-во | Кол-во категорий | Доля от сети | |
|---|---|---|---|---|---|---|
| 3038 | кафе | кафе | 159 | 189 | 7 | 84.1% |
| 3041 | кафе | ресторан | 8 | 189 | 7 | 4.2% |
| 3037 | кафе | быстрое питание | 7 | 189 | 7 | 3.7% |
| 3039 | кафе | кофейня | 6 | 189 | 7 | 3.2% |
| 3042 | кафе | столовая | 6 | 189 | 7 | 3.2% |
| 3036 | кафе | бар,паб | 2 | 189 | 7 | 1.1% |
| 3040 | кафе | пиццерия | 1 | 189 | 7 | 0.5% |
| 5393 | хинкальная | кафе | 19 | 44 | 5 | 43.2% |
| 5394 | хинкальная | ресторан | 15 | 44 | 5 | 34.1% |
| 5392 | хинкальная | быстрое питание | 6 | 44 | 5 | 13.6% |
| 5391 | хинкальная | бар,паб | 3 | 44 | 5 | 6.8% |
| 5395 | хинкальная | столовая | 1 | 44 | 5 | 2.3% |
| 5755 | шаурма | быстрое питание | 32 | 43 | 3 | 74.4% |
| 5756 | шаурма | кафе | 10 | 43 | 3 | 23.3% |
| 5754 | шаурма | булочная | 1 | 43 | 3 | 2.3% |
| 5526 | чайхана | кафе | 26 | 37 | 3 | 70.3% |
| 5527 | чайхана | ресторан | 9 | 37 | 3 | 24.3% |
| 5525 | чайхана | быстрое питание | 2 | 37 | 3 | 5.4% |
| 2221 | буханка | булочная | 25 | 32 | 3 | 78.1% |
| 2223 | буханка | кофейня | 6 | 32 | 3 | 18.8% |
| 2222 | буханка | кафе | 1 | 32 | 3 | 3.1% |
| 3913 | му-му | кафе | 12 | 27 | 7 | 44.4% |
| 3916 | му-му | ресторан | 8 | 27 | 7 | 29.6% |
| 3912 | му-му | быстрое питание | 2 | 27 | 7 | 7.4% |
| 3914 | му-му | кофейня | 2 | 27 | 7 | 7.4% |
| 3911 | му-му | бар,паб | 1 | 27 | 7 | 3.7% |
| 3915 | му-му | пиццерия | 1 | 27 | 7 | 3.7% |
| 3917 | му-му | столовая | 1 | 27 | 7 | 3.7% |
| 3450 | крошка картошка | быстрое питание | 20 | 22 | 3 | 90.9% |
| 1921 | андерсон | кафе | 17 | 22 | 3 | 77.3% |
| 1923 | андерсон | ресторан | 4 | 22 | 3 | 18.2% |
| 1922 | андерсон | кофейня | 1 | 22 | 3 | 4.5% |
| 3451 | крошка картошка | кафе | 1 | 22 | 3 | 4.5% |
| 3452 | крошка картошка | ресторан | 1 | 22 | 3 | 4.5% |
| 661 | french bakery | кафе | 17 | 20 | 3 | 85.0% |
| 4814 | скалка | булочная | 15 | 20 | 4 | 75.0% |
| 4816 | скалка | кофейня | 3 | 20 | 4 | 15.0% |
| 662 | french bakery | кофейня | 2 | 20 | 3 | 10.0% |
| 660 | french bakery | булочная | 1 | 20 | 3 | 5.0% |
| 4815 | скалка | кафе | 1 | 20 | 4 | 5.0% |
| 4817 | скалка | пиццерия | 1 | 20 | 4 | 5.0% |
| 1762 | wild bean cafe | кофейня | 8 | 16 | 3 | 50.0% |
| 1761 | wild bean cafe | кафе | 7 | 16 | 3 | 43.8% |
| 1760 | wild bean cafe | быстрое питание | 1 | 16 | 3 | 6.2% |
| 3205 | кафетерий | кафе | 11 | 15 | 3 | 73.3% |
| 3204 | кафетерий | быстрое питание | 2 | 15 | 3 | 13.3% |
| 3206 | кафетерий | столовая | 2 | 15 | 3 | 13.3% |
| 2105 | бистро | кафе | 6 | 12 | 4 | 50.0% |
| 2104 | бистро | быстрое питание | 4 | 12 | 4 | 33.3% |
| 2102 | бистро | бар,паб | 1 | 12 | 4 | 8.3% |
| 2103 | бистро | булочная | 1 | 12 | 4 | 8.3% |
Комментарий:
# Приведем все в нижний регистр
data.address = data.address.str.lower()
# Добавим столбец с адресами
data['street'] = data.address.str.split(', ').str[1]
# Добавим столбец с гордами
data['city'] = data.address.str.split(', ').str[0]
# Посчитаем кол-во уникальных улиц
data.street.nunique()
1447
data['address'].str.split(', ').str[1]
# Посчитаем кол-во уникальных городов
data.city.nunique()
1
Комментарий
# Приведем все в нижний регистр
data.hours.str.lower()
# Добавим столбец с булевымы значениями
data['clock'] = data.hours.str.contains('ежедневно, круглосуточно', regex=False, na=False)
# Найдем пропуски
df = data.isna().sum().reset_index()
# Добавим общее кол-во записей
df['all'] = data.shape[0]
df.columns = ['Col', 'items', 'all']
# Посчитаем долю
df['proc'] = df['items'] / df['all']
df.sort_values('items', ascending=False).head(10).style.format({'proc': '{:.2%}'})\
.background_gradient(subset=['proc'])
| Col | items | all | proc | |
|---|---|---|---|---|
| 11 | middle_coffee_cup | 7871 | 8406 | 93.64% |
| 10 | middle_avg_bill | 5257 | 8406 | 62.54% |
| 8 | price | 5091 | 8406 | 60.56% |
| 9 | avg_bill | 4590 | 8406 | 54.60% |
| 13 | seats | 3611 | 8406 | 42.96% |
| 4 | hours | 536 | 8406 | 6.38% |
| 15 | city | 0 | 8406 | 0.00% |
| 14 | street | 0 | 8406 | 0.00% |
| 12 | chain | 0 | 8406 | 0.00% |
| 0 | name | 0 | 8406 | 0.00% |
# Пропуски кол-ва посадочных мест для категорий
(data[data['seats'].isna() == True].groupby('category', as_index=False)['name']
.count().sort_values('name', ascending=False))
| category | name | |
|---|---|---|
| 3 | кафе | 1160 |
| 6 | ресторан | 773 |
| 4 | кофейня | 662 |
| 0 | бар,паб | 297 |
| 2 | быстрое питание | 254 |
| 5 | пиццерия | 206 |
| 7 | столовая | 151 |
| 1 | булочная | 108 |
# Пропуски кол-ва посадочных мест для категории ресторан
(data[(data['seats'].isna() == True) & (data['category'] =='ресторан')]
.groupby('name', as_index=False)['address']
.count().sort_values('address', ascending=False)).head(10)
| name | address | |
|---|---|---|
| 636 | яндекс лавка | 34 |
| 140 | prime | 17 |
| 543 | теремок | 10 |
| 288 | джонджоли | 7 |
| 530 | сушистор | 7 |
| 405 | моремания | 7 |
| 536 | татнефть кафе | 5 |
| 329 | кафе | 5 |
| 466 | ресторан | 4 |
| 173 | vasilchukí chaihona №1 | 4 |
# Проверим содержиться ли информация в middle_avg_bill взятая из avg_bill
data[(data.avg_bill.isna() == False) &
(data.middle_avg_bill.isna() == True) &
(data.avg_bill.str.contains('Средний счёт:', regex=False) == True)]\
['middle_avg_bill'].count()
0
Средний чек перенесен полностью, пропуски отсутствуют.
# Проверим содержиться ли информация в middle_avg_bill взятая из avg_bill
data[(data.avg_bill.isna() == False) &
(data.middle_coffee_cup.isna() == True) &
(data.avg_bill.str.contains('чашки', regex=False) == True)]\
['middle_coffee_cup'].count()
0
Цена чашки кофе перенесена полностью, пропуски отсутствуют.
Комментарий:
В результате импорта и предобработки данных было выполнено:
Напишем функции для построения графиков
# График БАР
def chart_bar(df, x, y, text, title, xa='', ya='', color=None, legend=None, yaxis=None):
fig_bar = px.bar(df, x=x, y=y, text=text, color=color)
fig_bar.update_layout(title=title,
xaxis_title = xa,
yaxis_title = ya,
legend_title=legend,
yaxis=yaxis)
fig_bar.show()
# График Pie
def chart_pie(labels, values, title, pull=[0.0]):
fig_pie = go.Figure(data=[go.Pie(labels=labels,
values=values,
pull=pull,
textinfo='label+percent')])
fig_pie.update_layout(width=600,
height=600,
margin=dict(l=100, r=0, t=50, b=0),
title=title)
fig_pie.update_traces(showlegend=False)
fig_pie.show()
# График Heatmap
def chatr_heatmap(df, title, xa = '', ya = ''):
fig_heat = px.imshow(df, text_auto=True, aspect="auto")
fig_heat.update_layout(title=title, xaxis_title = xa, yaxis_title = xa)
fig_heat.show()
# функция построения карты по районам
def geo_hit_map(df, locations, color, title, labels={'name':'Кол-во', 'district':'Наименование округа'}):
map_h = px.choropleth_mapbox(df,
geojson=geo_json,
locations=locations,
color=color,
color_continuous_scale="blugrn",
featureidkey="name",
mapbox_style="carto-positron",
zoom=8.5, center = {"lat": 55.7522, "lon": 37.6156}, opacity=0.5,
labels=labels)
map_h.update_layout(margin={"r":0,"t":50,"l":0,"b":0}, title=title)
map_h.show()
Подсчитаем количество заведений по категориям и построим графики для визуальной оценки их распределения.
# Считаем количество заведений по категориям
cat_hist = (data.groupby('category', as_index=False)['name']
.count().sort_values('name', ascending=False).reset_index(drop=True))
cat_hist.columns = ['Категория', 'Кол-во']
# Строим гистограмму
chart_bar(cat_hist, 'Категория', 'Кол-во', 'Кол-во', 'Самое большое количество заведений это КАФЕ',
'Категория', 'Кол-во')
# Строим график pie с процентным соотношением
chart_pie(cat_hist['Категория'], cat_hist['Кол-во'], 'Доля кафе самая большая', [0.1])
# pivot table for heatmap Кол-во заведений по округам
pt_1 = data.pivot_table(index='district', columns='category', values='name', aggfunc='count')
# heatmap
chatr_heatmap(pt_1, 'Кол-во заведений по округам', 'Категория', 'Округ')
# pivot table for heatmap Медианная оценка по округам
pt_2 = data.pivot_table(index='district', columns='category', values='rating',aggfunc='median')
chatr_heatmap(pt_2, 'Медианная оценка по округам', 'Категория', 'Округ')
print(f'''Всего {data["name"].count()} заведений
в {data.category.nunique()} категориях''')
Всего 8406 заведений в 8 категориях
Комментарий:
Да как и следовало ожидать жители столицы предпочитают пить кофе и посещать кафе/рестораны на бизнес ланч.
Посчитаем кол-во пропусков по категориям для посадочных мест и постоим графики распределения посадочных мест.
print('''
Из раздела 2.7 поле кол-во посадоычнх мест имеет 42.96% пропусков
или 3611 заведения из 8406.''')
df_setats_pr = (data[data.seats.isna() == True]
.groupby('category', as_index=False)['name'].count())
df_setats_npr = (data[data.seats.isna() == False]
.groupby('category', as_index=False)['name'].count())
df_setats_all = data.groupby('category', as_index=False)['name'].count()
df_setats = (df_setats_all.merge(df_setats_pr, how='left', on='category')
.merge(df_setats_npr, how='left', on='category'))
df_setats.columns = ['category', 'all', 'prop', 'not_prop']
df_setats = df_setats.sort_values('all', ascending=False)
df_setats['poroc'] = round(df_setats.prop / df_setats.not_prop * 100)
fig_set_prop = go.Figure(data=[
go.Bar(name='Нет пропусков', x=df_setats.category, y=df_setats.not_prop),
go.Bar(name='Есть пропуски', x=df_setats.category, y=df_setats.prop)
])
fig_set_prop.update_layout(title='В РЕСТОРАНАХ меньше всего пропусков',
yaxis_title = 'Кол-во заведений',
xaxis_title = 'Категория',
height=500)
fig_set_prop.update_layout(barmode='group')
fig_set_prop.show()
chart_bar(df_setats.sort_values('poroc'), 'category', 'poroc', 'poroc',
'Отношение пропусков к их отсутсвию', 'Категория', 'Процент')
# Постоим boxplot для посадоынх мест
fig_seats_box_all = px.box(data, y="seats", points="all", hover_data=['category', 'name'])
fig_seats_box_all.update_layout(title='75 посадочных мест - медианное значение',
yaxis_title = 'Кол-во',
xaxis_title = 'Все категории',
height=500)
fig_seats_box_all.show()
# Постоим boxplot для посадоынх мест с разбивкой на категории
fig_seats_box_cat = px.box(data, y="seats", x='category', range_y=[-10, 400], hover_data=['name'])
fig_seats_box_cat.update_layout(title='Распределение посадочных мест по категориям',
yaxis_title = 'Кол-во',
xaxis_title = 'Категория',
height=500)
fig_seats_box_cat.add_hline(y=75, line_width=1, line_dash="dash",
line_color="red", annotation_text="Общая медина")
fig_seats_box_cat.show()
sets = data[data.seats.isna() == False].copy()
sets['col_s'] = np.where(sets.seats < 100, '0-100',
np.where(sets.seats < 200, '100-200',
np.where(sets.seats < 300, '200-300',
np.where(sets.seats < 500, '300-500',
np.where(sets.seats < 800, '500-800',
np.where(sets.seats < 1200, '800-1200','более 1200' ))))))
s_300_map = px.scatter_mapbox(sets,
lat='lat',
lon='lng',
mapbox_style="open-street-map",
zoom=8.7,
hover_name='name',
hover_data=['seats'],
color='col_s',
labels={'col_s':'Кол-во мест'})
s_300_map.update_layout(margin={"r":0,"t":50,"l":0,"b":0},
title='Карта посадочных мест',)
s_300_map.update_traces(
marker=dict(size=13))
s_300_map.show()
Из раздела 2.7 поле кол-во посадоычнх мест имеет 42.96% пропусков или 3611 заведения из 8406.
# Расчитаем 96 перецентель
np.percentile(data[data.seats.isna() == False].seats, 96)
350.0
Комментарий:
Ресторанная сеть - это наличие нескольких предприятий общественного питания, которые могут охватывать и как региональный, так и международный рынки. Обычно, называют число три или пять. То есть три или пять заведений – это уже сеть. Примем в расчет, что 5 заведений это уже сеть.
# Посчитаем сколько раз каждое нзвание всетрчается раз
set_name = data.name.value_counts().reset_index()
# Уберем завдения встречающиеся менее 5 раз
set_name = set_name.query('name > 5')
# Переименуем колонки
set_name.columns = ['name', 'col']
# Посчитаем количество завведний
print('Количество уникальных сетевых заведений {}'.format(set_name.name.count()))
Количество уникальных сетевых заведений 107
Напишем имена которые явно не соответсвую сетевым
not_set = ['кафе', 'хинкальная', 'шаурма', 'чайхана', 'ресторан', 'столовая','донер кебаб', 'кафетерий',
'бистро', 'кофейня', 'кафе-столовая', 'кафе-бар', 'халяль', 'буфет', 'грузинская кухня',
'трапезная', 'кофе', 'хачапури', 'донер', 'тандыр', 'шашлычная']
spisok = set_name.name.to_list()
spisok = np.setdiff1d(spisok, not_set).tolist()
len(spisok)
86
data.query('name in @spisok').chain.value_counts()
1 1494 0 8 Name: chain, dtype: int64
В Список попали 8 не сетевых заведений, посмотрим на них
data.query('name in @spisok and chain == 0').name.value_counts()
star hit cafe 1 мск lounge 1 франклинс бургер 1 ля фантази 1 домино'с пицца 1 korean chick 1 one price coffee 1 burger club 1 Name: name, dtype: int64
Видим, что в данные закралась ошибка, все заведения сетевые
set_zaved = data.query('name in @spisok').name.count()
not_set_zaved = data.query('name not in @spisok').name.count()
chart_bar(df, ['Сетевое', 'Не сетеове'],
[set_zaved, not_set_zaved], [set_zaved, not_set_zaved],
'Рапределение сетевых заведений', 'Тип заведения', 'Кол-во')
chart_pie(['Сетевое', 'Не сетеове'], [set_zaved, not_set_zaved], 'Доля кафе самая большая', [0, 0.1])
Комментарий:
pt_3 = data.query('name in @spisok').category.value_counts().reset_index()
chart_bar(pt_3, 'index', 'category', 'category', 'Кофейня самое сетевое заведение', 'Тип заведения', 'Кол-во')
chart_pie(pt_3['index'], pt_3['category'], 'Доля кофейнь самая большая', pull=[0.1])
Комментарий:
data.name.value_counts().reset_index().head(15)
| index | name | |
|---|---|---|
| 0 | кафе | 189 |
| 1 | шоколадница | 120 |
| 2 | домино'с пицца | 77 |
| 3 | додо пицца | 74 |
| 4 | one price coffee | 72 |
| 5 | яндекс лавка | 69 |
| 6 | cofix | 65 |
| 7 | prime | 50 |
| 8 | хинкальная | 44 |
| 9 | шаурма | 43 |
| 10 | кофепорт | 42 |
| 11 | кулинарная лавка братьев караваевых | 39 |
| 12 | теремок | 38 |
| 13 | чайхана | 37 |
| 14 | ресторан | 34 |
В топ 15 входят заведения имя которых не принадлжит какой либо сети. Нужно их исключить. Так же входит сеть доставки Яндекс Лавка ее тоже убрем
top_15 = data.name.value_counts().reset_index().query('index != ["кафе", "хинкальная",\
"шаурма", "чайхана", "столовая", "яндекс лавка", "ресторан"]').head(15)
top_15.columns = ['name', 'col']
top_15
| name | col | |
|---|---|---|
| 1 | шоколадница | 120 |
| 2 | домино'с пицца | 77 |
| 3 | додо пицца | 74 |
| 4 | one price coffee | 72 |
| 6 | cofix | 65 |
| 7 | prime | 50 |
| 10 | кофепорт | 42 |
| 11 | кулинарная лавка братьев караваевых | 39 |
| 12 | теремок | 38 |
| 15 | буханка | 32 |
| 16 | cofefest | 32 |
| 18 | му-му | 27 |
| 19 | drive café | 24 |
| 20 | кофемания | 23 |
| 21 | крошка картошка | 22 |
chart_bar(top_15.sort_values('col'), 'col', 'name', 'col',
'ТОП-15 популярных сетей в Москве', 'Кол-во', 'Название сети')
top_15_cat =(data.query('name in @top_15.name.to_list()')
.groupby(['name','category'], as_index=False)['address']
.count())
top_15_cat['col'] = top_15_cat.groupby('name')['address'].transform('sum')
top_15_cat = top_15_cat.sort_values(['col', 'address'], ascending=False)
chart_bar(top_15_cat.sort_values('col'), 'address', 'name', None, 'Кофейни очень популярны',
'Кол-во', 'Категория', 'category', 'Заведение', {'categoryorder': 'total ascending'})
Комментарий:
# Загрузим границы административных округов
with urlopen('https://code.s3.yandex.net/data-analyst/admin_level_geomap.geojson') as f:
geo_json = json.load(f)
# Считаем заведения по округам
district = data.district.value_counts().reset_index()
district.columns = ['name', 'item']
district['proc'] = district.item / district.item.sum()
display(district.sort_values('item', ascending=False).style.format({'proc':'{:.2%}'}))
chart_bar(district, 'item', 'name', 'item', 'Центральный округ лидер по кол-ву заведений',
'Кол-во', 'Округа', yaxis={'categoryorder': 'total ascending'})
# Строим карту по количесву заведений
geo_hit_map(district, 'name', 'item', 'Кол-во заведений по округам', {'item':'Кол-во', 'name':'Округ'})
| name | item | proc | |
|---|---|---|---|
| 0 | Центральный административный округ | 2242 | 26.67% |
| 1 | Северный административный округ | 900 | 10.71% |
| 2 | Южный административный округ | 892 | 10.61% |
| 3 | Северо-Восточный административный округ | 891 | 10.60% |
| 4 | Западный административный округ | 851 | 10.12% |
| 5 | Восточный административный округ | 798 | 9.49% |
| 6 | Юго-Восточный административный округ | 714 | 8.49% |
| 7 | Юго-Западный административный округ | 709 | 8.43% |
| 8 | Северо-Западный административный округ | 409 | 4.87% |
Комментарий:
cat_rat = data.groupby('category', as_index=False).agg(median=('rating', 'median'),
mean=('rating', 'mean')).sort_values('median', ascending=False)
cat_rat['mean'] = cat_rat['mean'].round(1)
display(cat_rat)
fig_box_cat_rat = px.box(data, y='category', x='rating')
fig_box_cat_rat.update_layout(title='Распределение рейтенгов заведений',
xaxis_title = 'Оценка',
yaxis_title = 'Категория',
yaxis={'categoryorder': 'total ascending'})
fig_box_cat_rat.add_vline(x=data.rating.median(),
line_width=2, line_dash="dash", line_color="red",
annotation_text="Медиана по всем заведениям")
fig_box_cat_rat.show()
| category | median | mean | |
|---|---|---|---|
| 0 | бар,паб | 4.4 | 4.4 |
| 1 | булочная | 4.3 | 4.3 |
| 4 | кофейня | 4.3 | 4.3 |
| 5 | пиццерия | 4.3 | 4.3 |
| 6 | ресторан | 4.3 | 4.3 |
| 7 | столовая | 4.3 | 4.2 |
| 2 | быстрое питание | 4.2 | 4.1 |
| 3 | кафе | 4.2 | 4.1 |
Комментарий:
dist_rat = data.groupby('district', as_index=False)['rating'].median()
geo_hit_map(dist_rat, 'district', 'rating', 'Средняя оценка по округам',
labels={'rating':'Рейтинг','district':'Наименование округа'})
Комментарий:
all_map = px.scatter_mapbox(data,
lat='lat',
lon='lng',
mapbox_style="open-street-map",
zoom=8.7,
hover_name='name',
labels={'category':'Категория'})
all_map.update_layout(margin={"r":0,"t":50,"l":0,"b":0},
title='Все заведения',)
all_map.update_traces(cluster=dict(enabled=True,
opacity=0.5,
size=8,
color='red',
maxzoom=13),
marker=dict(size=13))
all_map.show()
top_street = (data.query('street != "мкад"').street.value_counts().reset_index()
.sort_values('street', ascending=False)
.head(15))
top_street.columns = ['streets', 'item']
chart_bar(top_street, 'item', 'streets', 'item', 'TОП-15 улиц', 'Кол-во', 'Улицы',
yaxis={'categoryorder': 'total ascending'})
top_street_map = px.scatter_mapbox(data.query('street in @top_street.streets.unique()'),
lat='lat',
lon='lng',
mapbox_style="carto-positron",
color='street',
zoom=8.7,
hover_name='name',
hover_data=['street'],
labels={'street':'Улица'})
top_street_map.update_layout(margin={"r":0,"t":50,"l":0,"b":0},
title='ТОП-15 улиц',)
top_street_map.update_traces(
marker=dict(size=5))
top_street_map.show()
Комментарий:
one_list_street = (data.street.value_counts().reset_index()
.query('street == 1')['index'].to_list())
one_street = data.query('street in @one_list_street')
print('Всего {} улиц с одним заведением'.format(len(one_list_street)))
print('Это {:.1%} от всех улиц'.format(len(one_list_street)/data.street.nunique()))
Всего 457 улиц с одним заведением Это 31.6% от всех улиц
on_pt = (one_street.pivot_table(index=['district', 'category'],
values=['name', 'rating', 'middle_avg_bill'],
aggfunc={'name':'count',
'rating':'median',
'middle_avg_bill':'median'})
.reset_index())
fig_on_pt = px.treemap(on_pt, path=[px.Constant("Москва"), 'district', 'category'], values='name',
color='rating',
labels={'rating':'Рейтинг',
'labels':'Тип',
'parent':'Округ',
'name':'Кол-во'},
color_continuous_scale='RdBu')
#color_continuous_midpoint=np.average(df['rating'], weights=df['name']))
fig_on_pt.update_layout(
title='Кол-во улиц с одним заведением по округам',
margin=dict(t=50, l=25, r=25, b=25))
fig_on_pt.show()
Комментарий:
dis_avg = data.groupby('district', as_index=False)['middle_avg_bill'].median()
geo_hit_map(dis_avg, 'district', 'middle_avg_bill', 'Средний чек в округе',
{'middle_avg_bill':'Средний чек','district':'Наименование округа'})
Комментарий:
clock_24 = data.clock.value_counts().reset_index()
chart_pie(['Не круглосуточно', 'Круглосуточно'], clock_24.clock.to_list(), 'Режим работы', pull=[0.1])
clock = data[data.clock == True].pivot_table(index='district', columns='category', values='name',
aggfunc='count').fillna(0)
chatr_heatmap(clock, 'Распределение круглосуточных заведений', 'Категория', 'Округ')
crt = data[data.clock == True].groupby('district')['name'].count().reset_index()
geo_hit_map(crt, 'district', 'name', 'Круглосуточные заведения', {'name':'Кол-во', 'district':'Наименование округа'})
Комментарий:
Общие сведения
Количество посадочных мест:
Доля сетевых заведений: В расчетах принято, что сетевое заведение - это кол-во заведений у одного имени более 5 и не название не соответствует таким как: кафе, шаурма и прочее.
В ТОП-15 сетевых заведений:
Рейтинг:
ТОП-15 улиц
Улица с одним заведением:
Средний чек:
Режим работы:
# Импортируем координаты метро
metro = pd.read_csv('https://github.com/VoytyukIlya/doc/blob/main/metro/624.csv?raw=true',
sep=';', index_col=0,)
metro = metro[['STATION','X', 'Y']]
metro['X'] = metro['X'].str.replace(',','.')
metro['Y'] = metro['Y'].str.replace(',','.')
cof = data[data.category == 'кофейня']
print('Всего кофеен в городе Москва {} кофейн'.format(cof.name.count()))
cof_pt = cof.groupby('district', as_index=False)['name'].count()
cof_pt = cof_pt.sort_values('name')
chart_bar(cof_pt, 'name', 'district', 'name', 'Кол-во кофеен по округам', 'Кол-во', 'Округ')
geo_hit_map(cof_pt, 'district', 'name', 'Кол-во кофеен по округам', {'name':'Кол-во','district':'Наименование округа'})
fig = go.Figure()
fig.add_trace(go.Scattermapbox(
lat=cof.lat,
lon=cof.lng,
mode='markers',
marker=go.scattermapbox.Marker(
size=6,
color='rgb(255, 0, 0)',
opacity=0.7
),
text=cof.name,
name='Кофейни'
))
fig.add_trace(go.Scattermapbox(
lat=metro['Y'],
lon=metro['X'],
mode='markers',
marker=go.scattermapbox.Marker(
size=8,
color='blue',
opacity=0.7
),
text=metro.STATION,
name='Метро'
))
fig.update_layout(
title='Расопложение относительно станций метро',
autosize=True,
hovermode='closest',
showlegend=True,
mapbox_style="open-street-map",
margin={"r":0,"t":50,"l":0,"b":0},
mapbox=dict(
center=dict(
lat=55.7522,
lon=37.6156
), zoom=9))
fig.show()
Всего кофеен в городе Москва 1413 кофейн
Комментарий:
cof_1 = cof[cof.clock==True]
cof_pt_map_24 = px.scatter_mapbox(cof_1,
lat='lat',
lon='lng',
mapbox_style="open-street-map",
zoom=8.7,
hover_name='name',
hover_data=['hours'],
labels={'street':'Улица'})
cof_pt_map_24.update_layout(margin={"r":0,"t":50,"l":0,"b":0},
title='Круглосуточные кофейни',)
cof_pt_map_24.update_traces(
marker=dict(size=12))
cof_pt_map_24.show()
cof_pt_map_24_ch = px.scatter_mapbox(cof_1.query('name == "шоколадница"'),
lat='lat',
lon='lng',
mapbox_style="open-street-map",
zoom=8.7,
hover_name='name',
hover_data=['hours'],
labels={'street':'Улица'})
cof_pt_map_24_ch.update_layout(margin={"r":0,"t":50,"l":0,"b":0},
title='Круглосуточные Шоколадницы',)
cof_pt_map_24_ch.update_traces(
marker=dict(size=12, color='red'))
cof_pt_map_24_ch.show()
print('Всего круглосуточных кофейн {}'.format(cof_1.name.count()))
print('Из них Шоколадниц {}'.format(cof_1.query('name == "шоколадница"').name.count()))
Всего круглосуточных кофейн 59 Из них Шоколадниц 17
Комментарий:
coffe_cup = px.box(data, x='category',
y='middle_coffee_cup',
range_y=[0, 400])
coffe_cup.update_layout(title='Распределение цен',
xaxis_title = 'Заведение',
yaxis_title = 'Цена')
coffe_cup.show()
cof_price = cof.groupby('district', as_index=False)['middle_coffee_cup'].median()
geo_hit_map(cof_price, 'district', 'middle_coffee_cup', 'Медианная цена по округам',
{'middle_coffee_cup':'Цена','district':'Наименование округа'})
cof_rat = cof.groupby('district', as_index=False)['rating'].mean()
geo_hit_map(cof_rat, 'district', 'rating', 'Рейтинг по округам',
{'rating':'Рейтинг', 'district':'Наименование округа'})
Комментарий:
Презентация: https://disk.yandex.ru/i/ZYk0bEmLTNhGJA